# frozen_string_literal: true

require 'spec_helper'

RSpec.describe GitlabSchema.types['Vulnerability'], feature_category: :vulnerability_management do
  let_it_be(:project) { create(:project) }
  let_it_be(:user) { create(:user) }
  let_it_be(:vulnerability) { create(:vulnerability, :with_remediation, project: project) }
  let(:vulnerabilities) { graphql_response.dig('data', 'project', 'vulnerabilities', 'nodes') }
  let(:query) do
    %(
        query {
          project(fullPath: "#{project.full_path}") {
            vulnerabilities {
              nodes {
                #{query_field}
              }
            }
          }
        }
      )
  end

  let_it_be(:fields) do
    %i[
      userPermissions
       id
       uuid
       title
       description
       descriptionHtml
       solution
       message
       user_notes_count
       state
       severity
       report_type
       resolved_on_default_branch
       vulnerability_path
       web_url
       location
       scanner
       primary_identifier
       identifiers
       project
       issueLinks
       detected_at
       confirmed_at
       resolved_at
       dismissed_at
       updated_at
       notes
       external_issue_links
       links
       has_remediations
       has_solutions
       false_positive
       state_comment
       merge_request
       discussions
       confirmed_by
       resolved_by
       dismissed_by
       details
       commenters
       message
       state_transitions
       dismissal_reason
    ]
  end

  RSpec.shared_examples "N+1 queries" do |single_query_count, multiple_queries_count|
    it 'avoids N+1 database queries', :request_store do
      GitlabSchema.execute(query, context: { current_user: user })

      control_count = ActiveRecord::QueryRecorder.new { GitlabSchema.execute(query, context: { current_user: user }) }.count
      expect(control_count).to eq(single_query_count)

      _create_three_vulnerabilities = create_list(:vulnerability, 3, :with_state_transition, :with_remediation, project: project)

      expect { GitlabSchema.execute(query, context: { current_user: user }) }.not_to exceed_query_limit(multiple_queries_count)
    end
  end

  before do
    stub_licensed_features(security_dashboard: true, sast_fp_reduction: true)

    project.add_developer(user)
  end

  subject(:graphql_response) { GitlabSchema.execute(query, context: { current_user: user }).as_json }

  it { expect(described_class).to have_graphql_fields(fields) }
  it { expect(described_class).to require_graphql_authorizations(:read_vulnerability) }

  describe 'vulnerability_path' do
    let(:query_field) { 'vulnerabilityPath' }

    it "is the path to the vulnerability's detail page" do
      expect(vulnerabilities.first['vulnerabilityPath']).to match(%r{^/#{project.full_path}/-/security/vulnerabilities/[0-9]+})
    end
  end

  describe 'stateComment' do
    let(:query_field) { 'stateComment' }
    let(:state_comment) { subject.dig(*%w[data project vulnerabilities nodes]).first["stateComment"] }

    let_it_be(:state_transition) { create(:vulnerability_state_transition, :from_detected, :to_dismissed, vulnerability: vulnerability) }

    it 'returns the comment for the latest state transition' do
      expect(state_comment).to eq(state_transition.comment)
    end

    context 'N+1 queries' do
      single_query_count = 10
      number_of_vulnerabilities = 3
      multiple_queries_count = single_query_count + (number_of_vulnerabilities * 3)

      it_behaves_like "N+1 queries", single_query_count, multiple_queries_count
    end
  end

  describe 'web_url' do
    let(:query_field) { 'webUrl' }

    it "is the URL to the vulnerability's detail page" do
      vulnerabilities = subject.dig('data', 'project', 'vulnerabilities', 'nodes')

      expect(vulnerabilities.first['webUrl']).to match(%r{^http://localhost/#{project.full_path}/-/security/vulnerabilities/[0-9]+})
    end
  end

  describe 'has_remediations' do
    let(:query_field) { 'hasRemediations' }

    context 'N+1 queries' do
      single_query_count = 10
      number_of_vulnerabilities = 3
      multiple_queries_count = single_query_count + (number_of_vulnerabilities * 2)

      it_behaves_like "N+1 queries", single_query_count, multiple_queries_count
    end
  end

  describe 'has_solutions' do
    let(:query_field) { 'hasSolutions' }

    context 'N+1 queries' do
      single_query_count = 10
      number_of_vulnerabilities = 3
      multiple_queries_count = single_query_count + (number_of_vulnerabilities * 3)

      it_behaves_like "N+1 queries", single_query_count, multiple_queries_count
    end
  end

  describe 'false_positive' do
    let_it_be(:vulnerability_with_finding) { create(:vulnerability, :with_findings, project: project) }
    let(:query_field) { 'falsePositive' }

    context 'when the vulnerability has a false-positive flag' do
      before do
        create(:vulnerabilities_flag, finding: vulnerability_with_finding.finding)
      end

      it 'returns true for false positive field', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/338073' do
        expect(vulnerabilities.first['falsePositive']).to be(true)
      end
    end

    context 'when the license is missing' do
      before do
        stub_licensed_features(security_dashboard: true, sast_fp_reduction: false)
      end

      it 'returns nil' do
        expect(vulnerabilities.first['falsePositive']).to be_nil
      end
    end

    context 'when the vulnerability does not have any false positive flag' do
      it 'returns false for false-positive field' do
        expect(vulnerabilities.first['falsePositive']).to be(false)
      end
    end
  end

  describe '#description' do
    let_it_be_with_reload(:vulnerability_with_finding) { create(:vulnerability, :with_findings, project: project) }
    let(:query_field) { 'description' }

    context 'when the vulnerability description field is populated' do
      it 'returns the description for the vulnerability' do
        expect(vulnerabilities.first['description']).to eq(vulnerability_with_finding.description)
      end
    end

    context 'when the vulnerability description field is empty' do
      before do
        vulnerability_with_finding.description = nil
        vulnerability_with_finding.save!
      end

      it 'returns the description for the vulnerability finding' do
        expect(vulnerabilities.first['description']).to eq(vulnerability_with_finding.finding.description)
      end
    end
  end

  describe '#descriptionHtml' do
    let_it_be(:vulnerability_with_finding) { create(:vulnerability, :with_findings, project: project) }
    let(:query_field) { 'descriptionHtml' }

    context 'when the vulnerability descriptionHtml field is populated' do
      it 'returns the description for the vulnerability' do
        expect(vulnerabilities.first['descriptionHtml']).to eq(vulnerability_with_finding.description_html)
      end
    end

    context 'when the vulnerability descriptionHtml field is empty' do
      before do
        vulnerability_with_finding.description_html = nil
        vulnerability_with_finding.save!
      end

      it 'returns the descriptionHtml for the vulnerability finding' do
        expect(vulnerabilities.first['descriptionHtml']).to eq(vulnerability_with_finding.description_html)
      end
    end
  end

  describe 'dismissal_reason' do
    let(:query_field) { 'dismissalReason' }
    let_it_be(:state_transition) { create(:vulnerability_state_transition, :from_detected, :to_dismissed, :used_in_tests, vulnerability: vulnerability) }

    before do
      # This simulates Vulnerabilities::DismissService
      vulnerability.update!(state: :dismissed, dismissed_by_id: user.id, dismissed_at: Time.current)
      vulnerability.vulnerability_read.update!(dismissal_reason: state_transition.dismissal_reason)
    end

    it 'is populated from Vulnerabilities::Read object' do
      expect(vulnerabilities.first['dismissalReason']).to eq("USED_IN_TESTS")
    end
  end

  describe 'merge_request' do
    let_it_be(:vulnerability_with_finding) { create(:vulnerability, :with_finding, project: project) }
    let_it_be(:merge_request) { create(:merge_request, source_project: project, description: 'Description for test') }
    let(:query_field) { 'mergeRequest { id }' }

    subject(:returned_mr_id) { vulnerabilities.first['mergeRequest']['id'] }

    before do
      create(:vulnerabilities_merge_request_link, vulnerability: vulnerability_with_finding, merge_request: merge_request)
    end

    it 'returns the merge request through merge_request_link' do
      expect(returned_mr_id).to eq(merge_request.to_global_id.uri.to_s)
    end
  end
end
